【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符

您所在的位置:网站首页 右值引用 构造函数 【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符

【UE4学习】20200826 C++11 移动语义、拷贝构造函数、赋值运算符

2024-07-13 00:52| 来源: 网络整理| 查看: 265

右值引用与TArray、TString参考文献 虚幻4与现代C++: 转移语义和右值引用 一次性搞定右值,右值引用(&&),和move语义 谈谈 C++ 中的右值引用 引子

前文讲到一个点:

12345// 差 - 返回常量数组const TArray GetSomeArray();// 优 - 返回常量数组的引用const TArray& GetSomeArray();

那为啥要这么写呢?这其中涉及到了右值引用这个知识点。

右值引用要解决啥问题

我们知道,C++中把值对象(包括栈上面分配的任何内存:基元类型或任何不是new出来的类实例)作为右值赋值给另外一个左值的时候,会触发复制。

比如:

12345678910111213141516171819202122class Test{public: int32 Value; /** 拷贝构造函数 */ Test(const Test& Another) { Value = Another.Value; }}Test Foo(){ Test Ret = Test(); return Ret;}int main(){ Test t = Foo();}

在上面这段代码中,要注意两点:

实际上Foo()真正返回到main函数中的对象和Foo中的Ret不是同一个,而是经过值复制得来的。 拿到Foo()的返回值之后,还需要执行拷贝构造函数,才能得到对象

这就造成了一个没有必要的消耗,也就是多了一次值复制的过程。

按照知乎帖子的神比喻来说:

如何将大象从一台冰箱转移到另一台冰箱?普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。

2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。

等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?

其实这样子设计也可以理解,毕竟Ret是分配于栈上的一个临时变量,当超出Foo()的定义域的时候就应该消失,此时外界还想要获取到该值,就应该通过值复制拷贝一份出去。

所以我们要解决这个不必要的值复制的问题。

怎么解决呢?临时变量不要被回收就好了。

既然临时变量要被回收,那么就意味着它不能再被其他任何地方用到。那么,在法律上属于被遗弃品,我们拾取了它,转移它的所有权即可。

所谓的右值引用,实质上就是一个「转移所有权」的过程,它把一个右值的所有权赋予了一个左值。(相对的,左值引用的意义比较简单,就是相当于给一个左值起了别名。)

左值右值傻傻分不清

看了那么多博客,这里直接说个结论吧。

所谓左值右值,乍看一下就是等号的左右边的值。其实不然,左右值的本质区别在于:

对于左值,指的是变量的存储位置 对于右值,指的是变量的值;可以是一个常量,也可以是函数返回的变量 右值引用

前面简单地提到了右值引用。所谓右值引用,可以简单地理解为「控制权转移」。

有了右值引用,C++世界变得有效率得多了。当使用右值赋值给左值的时候,可以直接将右值的控制权移交给左值,而不是先把右值复制一遍,再交给左值。

在右值引用之前,如果将参数传进来,一般用的是const左值引用,如下:

123456789void Foo(const string& str);void main(){ string a = "a"; Foo(a); // 没有复制,没有性能损耗 Foo("rvalue"); // 会产生一次转换}

如果我们编写了右值引用,那就会直接采用右值引用的版本

1void Foo(string&& str);

为了避免每次都要定义两个版本的函数,我们可以只定义一个右值引用的版本,然后采用std::move()把左值引用都变成右值引用。

右值引用代表着原拥有者放弃了所有权,由于一般都是临时变量,所以放弃了所有权也就放弃了,随便别人拿到所有权后咋整都行。

再举个例子。在右值引用和左值引用函数同时存在(重写了)时,如果传进来的是右值,会优先使用右值引用版本的函数。

1234567891011121314151617181920212223242526272829class Test{public: // 可能是一个大数组 int* Data; Test() : Data(new int[10000000]) {} Test(const Test& Another) { /**肯定不能够将Another的Data的地址赋值给自己,而是要进行深度复制, * 把整个数组复制一遍,才能够防止数据污染 */ std::copy(Another.Data, Another.Data + 10000000, this->Data); }}void Func(Test t){ // ...}void main(){ Person p; func(p); // Func的参数是值传递,会隐式调用`Test(const Test& Another)`来构建参数}

上面这个例子其实也没啥问题,传进去一个左值p,这个左值我们可能在main函数的定义域内还要做其他修改,当然不能够简单地浅复制解决。

但是假如我们是传进一个临时变量,那么再把这个大数组进行完整地复制,那就是多此一举了。最好的就是浅复制,直接拿到这个int数组。

1234void main(){ func(Person());}

这个时候我们要多提供一个右值引用的构造函数:

123456789/** * 进来这个版本的构造函数了,说明传进来的是右值,可以毫不留情地掠夺资源*/Test(Test&& Another){ this->Data = Another.Data; /** 把右值对象的内容置空,避免源对象析构时影响本对象 */ Another.Data = nullptr;}

如果我们能够保证左值在传入右值引用函数之后,不再使用,那么我们也可以把左值赋予给右值引用(抛弃自己的所有权,但是只能我们自己保证,编译器不会禁止继续使用和更改该左值)。

12345void main(){ Person p; func(move(p));} 禁止默认的const Class&构造函数1Test(const Test& Another) = delete;

如果我们delete了默认的拷贝构造函数,只实现移动构造函数,那么在使用拷贝构造函数的时候(不管隐式还是显示)就会编译报错,相当于强迫使用者使用右值引用。

1234Person a;Func(a); // error 左值不能转换成右值引用Func(std::move(a)); // ok a被转换成右值,但是最好保证后面不会在a的定义域再使用到aFunc(Person()); // ok 最后再举个例子12345678910void Func(){ Test ret; return ret;}int main(){ Test a = Func();}

上面这个过程总共发生了三次构造:

Func()中使用默认构造函数构建了左值ret Func()返回了一个值,众所周知函数的传入与传出都是要进行一次值复制的,所以进行一次复制 Test a使用Func()返回的结果作为参数传给构造函数

假设Test类中含有很大的一个数组,光是使用Test(const Test& Another)这种拷贝构造函数,我们没有办法区分清楚传进来的是左值还是右值,所以只能一律当做左值来处理。

如果是左值,那意味着传进来的这个引用,外界还会用到,并且有可能会对其内容做修改。所以我们需要进行深复制,把整个大数组拷贝一份。而如果传进来的是右值,那么亏了,明明我们知道这个右值被传进来使用过一次之后就会被外界销毁(右值都是一些会被销毁的临时变量),但是因为我们不知道究竟是不是右值,所以还是要把大数组整个拷贝一次。

而如果我们重写了一个Test(Test&& Another),当传进来的是一个右值的时候,会优先调用此构造函数。此时我们明确知道传进来的是一个右值,阅后即焚的那种,所以我们不必担心我们的修改会影响到这个变量。因此,我们直接把这个右值的内容拿过来用就行了。也就是说,直接把数组的地址拷贝过来,而不需要拷贝一整个数组。

拷贝构造函数&赋值运算符的区别

本质区别:拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。

换言之,一个会生成新的实例,而一个是对已存在实例进行数据更新。

1234567891011121314151617181920void Func(Person p){}void Func2(){ Person p; return p;}int main(){ Person p; Person p1 = p; // 构造 Person p2; p2 = p; // 复制 Func(p); // 构造 p2 = Func2(); // 先构造(`Func2`返回一个值对象),然后复制给p2 Person p3 = Func2(); // 本来是应该用构造创建一个函数返回值,然后再调用构造生成p3. //但是编译器优化过,导致直接用返回的右值赋予了给p3。}


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3